package com.hqd.ch03.v29.factory.support;

import com.hqd.ch03.utils.ObjectUtils;
import com.hqd.ch03.v29.AbstractSpringImitation;
import com.hqd.ch03.v29.beans.TypeConverter;
import com.hqd.ch03.v29.config.BeanDefinition;
import com.hqd.ch03.v29.factory.BeanFactoryUtils;
import com.hqd.ch03.v29.factory.config.RuntimeBeanNameReference;
import com.hqd.ch03.v29.factory.config.RuntimeBeanReference;
import com.hqd.ch03.v29.factory.config.TypedStringValue;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Array;
import java.util.*;

/**
 * 处理bean定义中的属性
 */
public class BeanDefinitionValueResolver {
    private final AbstractSpringImitation beanFactory;

    private final String beanName;

    private final BeanDefinition beanDefinition;

    private final TypeConverter typeConverter;


    public BeanDefinitionValueResolver(AbstractSpringImitation beanFactory, String beanName,
                                       BeanDefinition beanDefinition, TypeConverter typeConverter) {

        this.beanFactory = beanFactory;
        this.beanName = beanName;
        this.beanDefinition = beanDefinition;
        this.typeConverter = typeConverter;
    }

    protected Class<?> resolveTargetType(TypedStringValue value) throws ClassNotFoundException {
        if (value.hasTargetType()) {
            return value.getTargetType();
        }
        return value.resolveTargetType();
    }

    public Object resolveValueIfNecessary(Object argName, Object value) {
        if (value instanceof RuntimeBeanReference) {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            return resolveReference(argName, ref);
        } else if (value instanceof RuntimeBeanNameReference) {
            String refName = ((RuntimeBeanNameReference) value).getBeanName();
            refName = String.valueOf(doEvaluate(refName));
            if (!this.beanFactory.containsBean(refName)) {
                throw new RuntimeException(
                        "Invalid bean name '" + refName + "' in bean reference for " + argName);
            }
            return refName;
        } else if (value instanceof BeanDefinition) {
            BeanDefinition bd = (BeanDefinition) value;
            String innerBeanName = "(inner bean)" + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR +
                    ObjectUtils.getIdentityHexString(bd);
            return resolveInnerBean(argName, innerBeanName, bd);
        } else if (value instanceof ManagedArray) {
            ManagedArray array = (ManagedArray) value;
            Class<?> elementType = array.resolvedElementType;
            if (elementType == null) {
                String elementTypeName = array.getElementTypeName();
                if (StringUtils.isNotBlank(elementTypeName)) {
                    try {
                        elementType = Class.forName(elementTypeName);
                        array.resolvedElementType = elementType;
                    } catch (Throwable ex) {
                        throw new RuntimeException(ex.getMessage());
                    }
                } else {
                    elementType = Object.class;
                }
            }
            return resolveManagedArray(argName, (List<?>) value, elementType);
        } else if (value instanceof ManagedList) {
            return resolveManagedList(argName, (List<?>) value);
        } else if (value instanceof ManagedSet) {
            return resolveManagedSet(argName, (Set<?>) value);
        } else if (value instanceof ManagedMap) {
            return resolveManagedMap(argName, (Map<?, ?>) value);
        } else if (value instanceof ManagedProperties) {
            Properties original = (Properties) value;
            Properties copy = new Properties();
            original.forEach((propKey, propValue) -> {
                if (propKey instanceof TypedStringValue) {
                    propKey = evaluate(propKey);
                }
                if (propValue instanceof TypedStringValue) {
                    propValue = evaluate(propValue);
                }
                if (propKey == null || propValue == null) {
                    throw new RuntimeException(
                            "Error converting Properties key/value pair for " + argName + ": resolved to null");
                }
                copy.put(propKey, propValue);
            });
            return copy;
        } else if (value instanceof TypedStringValue) {
            TypedStringValue typedStringValue = (TypedStringValue) value;
            Object valueObject = evaluate(typedStringValue.getValue());
            try {
                Class<?> resolvedTargetType = resolveTargetType(typedStringValue);
                if (resolvedTargetType != null) {
                    return this.typeConverter.convertIfNecessary(valueObject, resolvedTargetType);
                } else {
                    return valueObject;
                }
            } catch (Throwable ex) {
                throw new RuntimeException(
                        "Error converting typed String value for " + argName, ex);
            }
        } else {
            return evaluate(value);
        }

    }

    private Object resolveManagedArray(Object argName, List<?> ml, Class<?> elementType) {
        Object resolved = Array.newInstance(elementType, ml.size());
        for (int i = 0; i < ml.size(); i++) {
            Array.set(resolved, i, resolveValueIfNecessary(new KeyedArgName(argName, i), ml.get(i)));
        }
        return resolved;
    }

    private List<?> resolveManagedList(Object argName, List<?> ml) {
        List<Object> resolved = new ArrayList<>(ml.size());
        for (int i = 0; i < ml.size(); i++) {
            resolved.add(resolveValueIfNecessary(new KeyedArgName(argName, i), ml.get(i)));
        }
        return resolved;
    }

    private Set<?> resolveManagedSet(Object argName, Set<?> ms) {
        Set<Object> resolved = new LinkedHashSet<>(ms.size());
        int i = 0;
        for (Object m : ms) {
            resolved.add(resolveValueIfNecessary(new KeyedArgName(argName, i), m));
            i++;
        }
        return resolved;
    }

    private Map<?, ?> resolveManagedMap(Object argName, Map<?, ?> mm) {
        Map<Object, Object> resolved = new LinkedHashMap<>(mm.size());
        mm.forEach((key, value) -> {
            Object resolvedKey = resolveValueIfNecessary(argName, key);
            Object resolvedValue = resolveValueIfNecessary(new KeyedArgName(argName, key), value);
            resolved.put(resolvedKey, resolvedValue);
        });
        return resolved;
    }

    private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefinition innerBd) {
        return null;
    }

    private Object resolveReference(Object argName, RuntimeBeanReference ref) {
        try {
            String resolvedName = String.valueOf(doEvaluate(ref.getBeanName()));
            return this.beanFactory.getBean(resolvedName);
        } catch (Exception ex) {
            throw ex;
        }
    }

    protected Object evaluate(Object value) {
        if (value instanceof String) {
            return doEvaluate((String) value);
        } else if (value instanceof String[]) {
            String[] values = (String[]) value;
            boolean actuallyResolved = false;
            Object[] resolvedValues = new Object[values.length];
            for (int i = 0; i < values.length; i++) {
                String originalValue = values[i];
                Object resolvedValue = doEvaluate(originalValue);
                if (resolvedValue != originalValue) {
                    actuallyResolved = true;
                }
                resolvedValues[i] = resolvedValue;
            }
            return (actuallyResolved ? resolvedValues : values);
        } else {
            return value;
        }
    }

    private Object doEvaluate(String value) {
        return this.beanFactory.evaluateBeanDefinitionString(value, this.beanDefinition);
    }

    @Data
    private static class KeyedArgName {

        private final Object argName;

        private final Object key;

        public KeyedArgName(Object argName, Object key) {
            this.argName = argName;
            this.key = key;
        }

    }
}
